package com.maolong.api2;

import com.maolong.framework.domain.Goods;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.range.ParsedDateRange;
import org.elasticsearch.search.aggregations.bucket.range.ParsedRange;
import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.ParsedAvg;
import org.elasticsearch.search.aggregations.metrics.ParsedMax;
import org.elasticsearch.search.aggregations.metrics.ParsedMin;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;

import static cn.hutool.poi.excel.sax.ElementName.v;

/**
 * 主要用来练习使用 ElasticSearchTemplate,
 * 同时通过封装为工具类的方式,方便以后的使用
 */
@Component
public class ElasticSearchTemplateUtil {
    @Autowired
    private static ElasticsearchRestTemplate restTemplate;


    /**
     *使用term精确查询,
     * QueryBuilders.termQuery("categoryName", "手机")
     * todo termQuery不支持集合查询嘛?不支持链式和集合
     */
    public static <T> SearchHits<T> termQuery(String field, String value, Class<T> tClass){
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(field, value);
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(termQueryBuilder)
                .build();
        SearchHits<T> search = restTemplate.search(nativeSearchQuery, tClass);
//        long totalHits = search.getTotalHits();
        search.getSearchHits().stream().map(searchHit -> searchHit.getContent()).forEach(System.out::println);
        return search;
    }
    /**
     * 使用terms,多个查询内容在一个字段中进行查询
     *termsQuery("categoryName", "手机", "平板电视")
     */
    public static <T> SearchHits<T> termsQuery(String field, List<String> values, Class<T> tClass){
        TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery(field, values);
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(termsQueryBuilder)
                .build();
        SearchHits<T> search = restTemplate.search(nativeSearchQuery, tClass);
        search.getSearchHits().stream().map(searchHit -> searchHit.getContent()).forEach(System.out::println);
        return search;
    }
    /**
     * 全文查询(match)
     * 全文查询会分析查询条件，先将查询条件进行分词，然后查询，求并集。
     * term和match的区别是：match是经过analyer的，也就是说，文档首先被分析器给处理了。
     * 根据不同的分析器，分析的结果也稍显不同，然后再根据分词结果进行匹配。term则不经过分词，它是直接去倒排索引中查找了精确的值了。
     */
    public static <T> SearchHits<T> matchQuery(String field, String value, Class<T> tClass){
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchQuery(field, value))
                .build();
        SearchHits<T> search = restTemplate.search(nativeSearchQuery, tClass);
        search.getSearchHits().stream().map(searchHit -> searchHit.getContent()).forEach(System.out::println);
        return search;
    }
    /**
     * 不封装了,冗余度比较高,将通过笔记类的形式来进行讲解
     */
    public void query(){
        /**
         * match
         * match_all：查询全部。
         * match：返回所有匹配的分词。
         * match_phrase：短语查询，在match的基础上进一步查询词组，可以指定slop分词间隔。
         * match_phrase_prefix：前缀查询，根据短语中最后一个词组做前缀匹配，可以应用于搜索提示，但注意和max_expanions搭配。其实默认是50.......
         * multi_match：多字段查询，使用相当的灵活，可以完成match_phrase和match_phrase_prefix的工作。
         */
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "Apple IPhone 白色")
                .analyzer("ik_smart").operator(Operator.AND);
        //查询全部,默认10条
        //查询条件(词条查询：对应ES query里的match)
        MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
//        match_phrase：短语查询，在match的基础上进一步查询词组，可以指定slop分词间隔。默认查询10条,不懂啥意思
        MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("title", "华为").slop(19);
//        multi_match：多字段查询，使用相当的灵活，可以完成match_phrase和match_phrase_prefix的工作
        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders
                .multiMatchQuery("华为和Apple", "title", "categoryName")
                .analyzer("ik_smart");
        /**
         * 通配符匹配
         * 通配符查询(wildcard)
         *  * *：表示多个字符（0个或多个字符）
         *  * ?：表示单个字符
         */
        WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("华为*", "title");
        /**
         * 模糊查询
         * 模糊查询所有以 “三” 结尾的商品信息
         */
        FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("title", "三").fuzziness(Fuzziness.AUTO);

        /**
         * 排序查询
         * 要分词的字段不可以直接排序，比如：text类型，如果想要对这类字段进行排序，需要特别设置
         * 对字段索引两次，一次索引分词（用于搜索）一次索引不分词（用于排序），es默认生成的text类型字段就是通过这样的方法实现可排序的。
         */
        MatchAllQueryBuilder matchAll = QueryBuilders.matchAllQuery();
        FieldSortBuilder fieldSortBuilder = SortBuilders.fieldSort("price").order(SortOrder.DESC);
        //创建查询条件构建器SearchSourceBuilder(对应ES外面的大括号)
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(matchAll)
                .withSort(fieldSortBuilder)
                .build();
        /**
         * 分页查询
         */
        MatchAllQueryBuilder matchAllQuery = QueryBuilders.matchAllQuery();
        FieldSortBuilder fieldSort = SortBuilders.fieldSort("price").order(SortOrder.DESC);
        PageRequest pageRequest = PageRequest.of(0, 10);
        NativeSearchQuery nativeSearch = new NativeSearchQueryBuilder()
                .withQuery(matchAllQueryBuilder)
                .withSort(fieldSortBuilder)
                .withPageable(pageRequest)
                .build();
        /**
         * 范围查询
         */
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").from(1000).to(2000);
        /**
         * bool查询'
         * bool 查询可以用来合并多个条件查询结果的布尔逻辑，它包含一下操作符：
         * must：多个查询条件必须完全匹配，相当于关系型数据库中的 and。
         * should：至少有一个查询条件匹配，相当于关系型数据库中的 or。
         * must_not： 多个查询条件的相反匹配，相当于关系型数据库中的 not。
         * filter：过滤满足条件的数据。
         * range：条件筛选范围。
         * gt：大于，相当于关系型数据库中的 >。
         * gte：大于等于，相当于关系型数据库中的 >=。
         * lt：小于，相当于关系型数据库中的 <。
         * lte：小于等于，相当于关系型数据库中的 <=
         */
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("title", "金立"))
                .must(QueryBuilders.matchQuery("categoryName", "手机"))
                .filter(QueryBuilders.rangeQuery("price").gt(1000).lt(2000));
        /**
         * queryString查询
         * 会对查询条件进行分词， 然后将分词后的查询条件和词条进行等值匹配，
         * 默认取并集（OR），可以指定单个字段也可多个查询字段
         */
        QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery("华为手机").defaultOperator(Operator.AND);

        /**
         * 结果过滤
         * 我们在查询数据的时候，返回的结果中，所有字段都给我们返回了，但是有时候我们并不需要那么多，所以可以对结果进行过滤处理。
         */
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("title", "金立"))
                .must(QueryBuilders.matchQuery("categoryName", "手机"))
                .filter(QueryBuilders.rangeQuery("price").gt(1000).lt(2000));

        // 不需要获取source结果集
        SourceFilter sourceFilter = new FetchSourceFilterBuilder().withIncludes("title", "categoryName").build();

        //创建查询条件构建器SearchSourceBuilder(对应ES外面的大括号)
        NativeSearchQuery nativeSearch2 = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                .withSourceFilter(sourceFilter)
                .build();
        /**
         * 高亮查询
         */
        HighlightBuilder highlightBuilder = new HighlightBuilder().field("title")
                .preTags("<font color='red'>").postTags("</font>");
        //创建查询条件构建器SearchSourceBuilder(对应ES外面的大括号)
        NativeSearchQuery nativeSearch3 = new NativeSearchQueryBuilder()
                .withQuery(matchQueryBuilder)
                .withHighlightBuilder(highlightBuilder)
                .build();
        SearchHits<Goods> search = restTemplate.search(nativeSearch3, Goods.class);
        search.getSearchHits().stream().map(searchHit -> {
            //获得结果实体
            Goods goods = searchHit.getContent();
            //所有高亮结果
            Map<String, List<String>> highlightFields = searchHit.getHighlightFields();
            //遍历高亮结果
            for (Map.Entry<String, List<String>> stringListEntry : highlightFields.entrySet()) {
                String key = stringListEntry.getKey();
                //获取实体反射类
                Class<?> aClass = goods.getClass();
                try {
                    //获取该实体属性
                    Field declaredField = aClass.getDeclaredField(key);
                    //权限为私的 解除！
                    declaredField.setAccessible(true);
                    //替换，把高亮字段替换到这个实体对应的属性值上
                    declaredField.set(goods, stringListEntry.getValue().get(0));
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return goods;
        }).forEach(System.out::println);




    }
    /**
     * 滚动查询
     * 滚动查询可以优化ES的深度分页，但是需要维护scrollId
     */
    public void scrollQuery(){

    }

    /**
     * 聚合查询
     * 我们平时在使用Elasticsearch时，更多会用到聚合操作，它类似SQL中的group by操作。ES的聚合查询一定是先查出结果，
     * 然后对结果使用聚合函数做处理，常用的操作有：avg：求平均、max：最大值、min：最小值、sum：求和等。
     *
     * 在ES中聚合分为指标聚合和分桶聚合：
     * Metric 指标聚合：指标聚合对一个数据集求最大、最小、和、平均值等
     * Bucket 分桶聚合：除了有上面的聚合函数外，还可以对查询出的数据进行分组group by，再在组上进行游标聚合。
     */
    public void aggregationQuery(){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 聚合条件
        queryBuilder.addAggregation(AggregationBuilders.max("maxPrice").field("price"));
        queryBuilder.addAggregation(AggregationBuilders.min("minPrice").field("price"));
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
        //查询,获取查询结果
        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));
        //获取聚合结果
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        //打印聚合结果
        ParsedMax max = aggregations.get("maxPrice");
        System.out.println("最贵的价格：" + max.getValue());
        ParsedMin min = aggregations.get("minPrice");
        System.out.println("最便宜的价格：" + min.getValue());
    }
    /**
     *Bucket 分桶聚合分析
     * 根据商品分类聚合查询
     */
    public void aggBucket() {

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 聚合条件
        queryBuilder.addAggregation(AggregationBuilders.terms("aggCategoryName").field("categoryName").size(10));
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
        //查询,获取查询结果
        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));
        //获取聚合结果
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        ParsedStringTerms aggCategoryName = aggregations.get("aggCategoryName");
        //打印聚合结果
        System.out.println(aggCategoryName.getBuckets());
        for (Terms.Bucket bucket : aggCategoryName.getBuckets()) {
            System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount());
        }
    }

    /**
     * 根据价格区间分组查询
     */

    public void aggRange() {

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        queryBuilder.addAggregation(AggregationBuilders.range("priceRange").field("price").addUnboundedTo(1000).addRange(1000, 3000).addUnboundedFrom(3000));
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());

        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));

        // 获取聚合信息
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        ParsedRange priceRange = aggregations.get("priceRange");

        //获取总记录数
        System.out.println("totalHits = " + searchHits.getTotalHits());

        //获取值返回
        for (Range.Bucket bucket : priceRange.getBuckets()) {
            System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount());
        }
    }

    /**
     * 根据日期分组查询出商品创建日期在"2017-09" - "2017-10" 之间的数据
     */

    public void aggDateRange() {

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        // range查询包左不包右,即:[1,10)
        queryBuilder.addAggregation(AggregationBuilders.dateRange("dateRange").field("createTime").format("yyy-MM").addRange("2017-09", "2017-10"));
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());

        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));

        // 获取聚合信息
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        ParsedDateRange priceRange = aggregations.get("dateRange");

        //获取总记录数
        System.out.println("totalHits = " + searchHits.getTotalHits());

        //获取值返回
        for (Range.Bucket bucket : priceRange.getBuckets()) {
            System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount());
        }
    }


    /**
     * 根据品牌聚合获取出每个品牌的平均价格
     */
    public void subAgg() {

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        queryBuilder.addAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName")
                .subAggregation(AggregationBuilders.avg("avgPrice").field("price")));

        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));

        // 获取聚合信息
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        ParsedStringTerms brandeNameAgg = aggregations.get("brandNameAgg");

        //获取总记录数
        System.out.println("totalHits = " + searchHits.getTotalHits());

        //获取值返回
        for (Terms.Bucket bucket : brandeNameAgg.getBuckets()) {

            // 获取聚合后的品牌名称
            String brandName = bucket.getKeyAsString();
            // 获取聚合命中的文档数量
            long docCount = bucket.getDocCount();
            // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象
            ParsedAvg avgPrice = bucket.getAggregations().get("avgPrice");

            System.out.println(brandName + "======" + avgPrice.getValue() + "======" + docCount);
        }
    }
    /**
     * 根据商品分类聚合，获取每个商品类的平均价格，并且在商品分类聚合之上子聚合每个品牌的平均价格
     */

    public void subSubAgg() {

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        // 注意这里聚合写的位置不要写错,很容易搞混,错一个括号就不对了
        queryBuilder.addAggregation(
                AggregationBuilders.terms("categoryNameAgg").field("categoryName")
                        .subAggregation(AggregationBuilders.avg("categoryNameAvgPrice").field("price"))
                        .subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName")
                                .subAggregation(AggregationBuilders.avg("brandNameAvgPrice").field("price"))));

        SearchHits<Goods> searchHits = restTemplate.search(queryBuilder.build(), Goods.class, IndexCoordinates.of("goods"));

        // 获取聚合信息
        Aggregations aggregations = searchHits.getAggregations();
        assert aggregations != null;
        ParsedStringTerms categoryNameAgg = aggregations.get("categoryNameAgg");

        //获取总记录数
        System.out.println("totalHits = " + searchHits.getTotalHits());

        //获取值返回
        for (Terms.Bucket bucket : categoryNameAgg.getBuckets()) {

            // 获取聚合后的分类名称
            String categoryName = bucket.getKeyAsString();
            // 获取聚合命中的文档数量
            long docCount = bucket.getDocCount();
            // 获取聚合后的分类的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象
            ParsedAvg avgPrice = bucket.getAggregations().get("categoryNameAvgPrice");

            System.out.println(categoryName + "======" + avgPrice.getValue() + "======" + docCount);

            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandNameAgg");
            for (Terms.Bucket brandeNameAggBucket : brandNameAgg.getBuckets()) {
                // 获取聚合后的品牌名称
                String brandName = brandeNameAggBucket.getKeyAsString();

                // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象
                ParsedAvg brandNameAvgPrice = brandeNameAggBucket.getAggregations().get("brandNameAvgPrice");

                System.out.println("   " + brandName + "======" + brandNameAvgPrice.getValue());
            }
        }
    }


}
